<?php
/**
 * Plugin Name: AJAX Hits Counter + Popular Posts Widget
 * Plugin URI: http://romantelychko.com/downloads/wordpress/plugins/ajax-hits-counter.latest.zip
 * Description: Counts page/posts hits via AJAX and display it in admin panel. Ideal for nginx-whole-page-caching. Popular Posts Widget included.
 * Version: 0.7.1
 * Author: Roman Telychko
 * Author URI: http://romantelychko.com
*/

///////////////////////////////////////////////////////////////////////////////

function ajax_hits_counter_init( $content )
{
    if( is_single() ) 
    {
        $content .= ajax_hits_counter_build_ajax();
    }
    
    return $content;
}

function ajax_hits_counter_build_ajax()
{
    global $post;

    return        
        '<script type="text/javascript">'.
            'function ahc_getXmlHttp(){var e;try{e=new ActiveXObject("Msxml2.XMLHTTP")}catch(t){try{e=new ActiveXObject("Microsoft.XMLHTTP")}catch(n){e=false}}if(!e&&typeof XMLHttpRequest!="undefined"){e=new XMLHttpRequest}return e};'.
            'var ahc_xmlhttp=ahc_getXmlHttp();'.            
            'ahc_xmlhttp.open("GET","'.admin_url( 'admin-ajax.php' ).'?action=ajax-hits-counter-increment&post_id='.$post->ID.'",true);'.
            'ahc_xmlhttp.send(null);'.
        '</script>';
}

function ajax_hits_counter_increment() 
{    
    if( !isset($_GET['post_id']) || empty($_GET['post_id']) )
    {
        die( '0' );
    }    
    
    $post_id = intval( filter_var( $_GET['post_id'], FILTER_SANITIZE_NUMBER_INT ) );
    
    if( empty($post_id) )
    {
        die( '0' );
    }

    $current_hits = get_post_meta( $post_id, 'hits', true );
    
    if( empty($current_hits) ) 
    {
        $current_hits = 0;
    }
    
    $current_hits++;
        
    update_post_meta( $post_id, 'hits', $current_hits );
    
    die( strval( $current_hits ) );
}

///////////////////////////////////////////////////////////////////////////////

function ajax_hits_counter_posts_table_column( $column )
{
    $column['hits'] = 'Hits';    

    return $column;
}

function ajax_hits_counter_posts_table_row( $column_name, $post_id )
{
    if( $column_name=='hits' )
    {
        $current_hits = get_post_meta( $post_id, 'hits', true );
        
        if( empty($current_hits) ) 
        {
            $current_hits = 0;
        }
        
        echo( $current_hits );
    }
}

function ajax_hits_counter_posts_table_sortable( $column )
{
    $column['hits'] = 'hits';    

    return $column;
}

function ajax_hits_counter_posts_table_orderby( $vars )
{
	if( isset($vars['orderby']) && $vars['orderby']=='hits' ) 
	{
		$vars = array_merge( 
    		    $vars, 
    		    array(
			        'meta_key'  => 'hits',
			        'orderby'   => 'meta_value_num'
        		    ) 
    		    );
	}
 
	return $vars;
}

///////////////////////////////////////////////////////////////////////////////

add_filter('the_content',       'ajax_hits_counter_init',       100);

///////////////////////////////////////////////////////////////////////////////

add_action( 'wp_ajax_nopriv_ajax-hits-counter-increment',   'ajax_hits_counter_increment' );
add_action( 'wp_ajax_ajax-hits-counter-increment',          'ajax_hits_counter_increment' );

///////////////////////////////////////////////////////////////////////////////

add_filter( 'manage_posts_columns',                         'ajax_hits_counter_posts_table_column' );
add_filter( 'manage_posts_custom_column',                   'ajax_hits_counter_posts_table_row', 10, 2 );
add_filter( 'manage_edit-post_sortable_columns',            'ajax_hits_counter_posts_table_sortable' );
add_filter( 'request',                                      'ajax_hits_counter_posts_table_orderby' );

///////////////////////////////////////////////////////////////////////////////

/**
 * Adds ajax_hits_counter_popular_posts_widget widget.
 */
class AJAX_Hits_Counter_Popular_Posts_Widget extends WP_Widget 
{
    ///////////////////////////////////////////////////////////////////////////
    
    protected $defaults = array(
        'widget_id'                 => 'ajax_hits_counter_popular_posts_widget',
        'sorting_algorithm'         => 1,               // hits only
        'count'                     => 5,
        'cache_lifetime'            => 3600,
        'one_element_html'          => "<span class=\"entry-content\">\n  <a href=\"{permalink}\" title=\"{post_title}\">{post_title} ({post_hits})</a>\n</span>",
        'post_categories_separator' => ', ',
        'post_date_format'          => 'd.m.Y',
        );

    ///////////////////////////////////////////////////////////////////////////

	/**
	 * Register widget with WordPress.
	 */
	public function __construct() 
	{
		parent::__construct(
	 		$this->defaults['widget_id'],
			'AJAX Hits Counter: Popular Posts',
			array(
			    'description'   => 'Displays popular posts counted by AJAX Hits Counter.', 
			    'classname'     => $this->defaults['widget_id'],
			    ),
		    array(
			    'width'     => 400,
			    'height'    => 350,
		    )
		);
	}
	
    ///////////////////////////////////////////////////////////////////////////

	/**
	 * Front-end display of widget.
	 *
	 * @see WP_Widget::widget()
	 *
	 * @param       array       $args               Widget arguments.
	 * @param       array       $instance           Saved values from database.
	 */
	public function widget( $args, $instance ) 
	{
	    // args
	    $args = array_merge( $this->defaults, $args );
		
        // cache key
        $cache_key = 'ajax_hits_counter_'.dechex(crc32( $args['widget_id'] ));

        // try to get cached data from transient cache
        $cache = get_transient( $cache_key );

        if( !is_array($cache) && !empty($cache) )
        #if( false )
        {   
            // cache exists, return cached data
            echo( $cache );            
            return true;
        }
        
        // get popular posts
	    $popular_posts = $this->getPopularPosts( $instance );
	    
		if( empty($popular_posts) )
		{
		    return false;
		}
		
		$title = apply_filters( 'widget_title', $instance['title'] );
 
        $output = $args['before_widget'];

		if( !empty( $title ) )
		{
			$output .= $args['before_title'].$title.$args['after_title'];
		}
		
		$output .= $this->getHTML( $popular_posts, $instance );

		$output .= $args['after_widget'];
	
        // store result to cache
        set_transient( $cache_key, $output, $args['cache_lifetime'] );	
		
		echo( $output );
	}
	
    ///////////////////////////////////////////////////////////////////////////
	
	/**
	 * Returns Popular Posts
	 *
	 * @param       array       $args
	 * @return      array
	 */
	public function getPopularPosts( $args = array() )
	{
	    global $wpdb;
	    
	    if( isset($args['sorting_algorithm']) )
	    {
            switch( $args['sorting_algorithm'] )
            {
                case 1:         // hits only
                default:
                    $sql_sorting_algorithm = '( m.meta_value + 0 ) DESC,';
                    break;
                
                case 2:         // comments only
                    $sql_sorting_algorithm = '( p.comment_count + 0 ) DESC,';
                    break;
                
                case 3:         // hits + comments * 10
                    $sql_sorting_algorithm = '( ( m.meta_value + 0 ) + ( p.comment_count + 0 ) * 10 ) DESC,';
                    break;
            }
        }
        else
        {
            $sql_sorting_algorithm = '( m.meta_value + 0 ) DESC,';
        }

	    $q = '
	        SELECT
		        DISTINCT p.ID,
		        p.post_title,
		        p.post_excerpt,
		        p.post_content,
		        p.post_author,
		        p.post_date,
		        m.meta_value        as post_hits,
		        p.comment_count     as post_comments_count
	        FROM
		        '.$wpdb->posts.' p
            JOIN
                '.$wpdb->postmeta.' m ON ( p.ID = m.post_id )
	        WHERE	
		        p.post_date_gmt < \''.date( 'Y-m-d H:i:s' ).'\'
		        AND
		        p.post_status = \'publish\' 
		        AND
		        p.post_type = \'post\'
	            AND
	            m.meta_key = \'hits\'
	        ORDER BY '.
	            $sql_sorting_algorithm.
		        'p.post_date_gmt DESC
	        LIMIT
	            '.$args['count'];

	    return 
	        $wpdb->get_results($q);	
	}
	
    ///////////////////////////////////////////////////////////////////////////
	
	/**
	 * Returns HTML of Popular Posts
	 *
	 * @param       array       $popular_posts
	 * @param       array       $args
	 * @return      string
	 */
	public function getHTML( $popular_posts = array(), $args = array() )
	{	
	    if( empty($popular_posts) )
	    {
	        return false;
	    }
	    
	    // fix bug in Wordpress :-)
        global $post;
        $tmp_post = $post;
	    
	    // args
	    $args = array_merge( $this->defaults, $args );
	    
	    $excerpt_length_isset = false;
	    
	    $html = '<ul>';
	    
	    foreach( $popular_posts as $post )
	    {	    
	        $post_author_obj = get_userdata( $post->post_author );
	        
	        $post_author_name = $post_author_obj->display_name;
	        $post_author_link = get_author_posts_url( $post_author_obj->ID, $post_author_obj->user_nicename );
	        
	        setup_postdata($post);

	        $temp_html = 
                str_ireplace(
                    array(
	                    '{post_id}',
	                    '{post_title}',
	                    '{post_author}',
	                    '{post_author_link}',
	                    '{permalink}',
	                    '{post_date}',	             
	                    '{post_hits}',       
	                    '{post_comments_count}',
	                    ),
                    array(
                        $post->ID,
                        //$post->post_title,
                        get_the_title(),
                        $post_author_name,
                        $post_author_link,
                        get_permalink($post->ID),
                        date( $args['post_date_format'], strtotime($post->post_date) ),
                        $post->post_hits,
                        $post->post_comments_count,
                        ),
                    $args['one_element_html']
                    );
                    
            if( preg_match_all( '#(\{thumbnail\-([^\}]+)\})#sim', $temp_html, $matches ) )
            {
                if( isset($matches['2']) && !empty($matches['2']) )
                {
                    foreach( $matches['2'] as $m )
                    {
                        $size = $m;
                    
                        if( preg_match( '#([0-9]+)x([0-9]+)#i', $m, $sizes ) )
                        {
                            if( isset($sizes['1']) && isset($sizes['2']) )
                            {
                                $size = array( $sizes['1'], $sizes['2'] );
                            }
                        }
                        
                        $temp_html = str_ireplace( '{thumbnail-'.$m.'}', get_the_post_thumbnail( $post->ID, $size ), $temp_html );
                    }
                }
            }
            
            if( stripos( $args['one_element_html'], '{post_categories}' )!==false )
            {
                $categories = get_the_category( $post->ID );
                
                if( !empty($categories) )
                {
                    $temp = array();
                
                    foreach( $categories as $category )
                    {
                        $temp[] = '<a href="'.get_category_link( $category->term_id ).'" title="'.esc_attr( $category->cat_name ).'">'.$category->cat_name.'</a>';
                    }
                    
	                $temp_html = str_ireplace( '{post_categories}', join( $args['post_categories_separator'], $temp ), $temp_html );
                }
            }

            if( preg_match( '#(\{post\_excerpt\_([0-9]+)\})#sim', $temp_html, $matches ) )
            {
                if( isset($matches['2']) && !empty($matches['2']) )
                {
                    $excerpt_length = intval($matches['2']);

                    if( $excerpt_length > 0 )
                    {              
                        if( $excerpt_length_isset===false )
                        {
                            add_filter( 'excerpt_length', create_function( '', 'return '.$excerpt_length.';' ), 1024 );
                            
                            $excerpt_length_isset = true;
                        }
                    }
                    
                    $temp_html = str_ireplace( $matches['1'], get_the_excerpt(), $temp_html );
                }            
            }
	    
	        $html .= '<li>'.$temp_html.'</li>';
	    }

	    $html .= '</ul>';
	    
	    // restore $post (Wordpress bug fixing)
	    wp_reset_postdata();
	    $post = $tmp_post;
	    
	    return $html;
	}
	
	///////////////////////////////////////////////////////////////////////////
	
	/**
	 * Clear transient widget cache
	 *
	 * @return      bool
	 */
	public function clearCache()
	{
	    global $wpdb;
	
	    $q = '
	        SELECT
		        option_name     as name
	        FROM
		        '.$wpdb->options.'
	        WHERE	
	            option_name LIKE \'_transient_ajax_hits_counter_%\'';

	    $transients = $wpdb->get_results($q);
	    
	    if( !empty($transients) )
	    {
	        foreach( $transients as $transient )
	        {
	            delete_transient( str_replace( '_transient_', '', $transient->name ) );
	        }
	    }
	    
	    return true;
	}
	
    ///////////////////////////////////////////////////////////////////////////

	/**
	 * Sanitize widget form values as they are saved.
	 *
	 * @see WP_Widget::update()
	 *
	 * @param       array       $new_instance       Values just sent to be saved.
	 * @param       array       $old_instance       Previously saved values from database.
	 *
	 * @return      array                           Updated safe values to be saved.
	 */
	public function update( $new_instance, $old_instance ) 
	{
	    // drop cache
	    $this->clearCache();
	
	    // return sanitized data
		return array(
		    'title'                         => trim( strip_tags( $new_instance['title'] ) ),
            'sorting_algorithm'             => intval( preg_replace( '#[^0-9]#', '', $new_instance['sorting_algorithm'] ) ),
		    'count'                         => intval( preg_replace( '#[^0-9]#', '', $new_instance['count'] ) ),
		    'cache_lifetime'                => intval( preg_replace( '#[^0-9]#', '', $new_instance['cache_lifetime'] ) ),
		    'one_element_html'              => trim( $new_instance['one_element_html'] ),
		    'post_categories_separator'     => $new_instance['post_categories_separator'],
		    'post_date_format'              => trim( strip_tags( $new_instance['post_date_format'] ) ),
		);
	}
	
    ///////////////////////////////////////////////////////////////////////////

	/**
	 * Back-end widget form.
	 *
	 * @see WP_Widget::form()
	 *
	 * @param       array       $instance           Previously saved values from database.
	 */
	public function form( $instance ) 
	{
	    // defaults
	    $title                      = __('Popular Posts');
	    $sorting_algorithm          = $this->defaults['sorting_algorithm'];
	    $count                      = $this->defaults['count'];
	    $cache_lifetime             = $this->defaults['cache_lifetime'];
	    $one_element_html           = $this->defaults['one_element_html'];
	    $post_categories_separator  = $this->defaults['post_categories_separator'];
	    $post_date_format           = $this->defaults['post_date_format'];
	
		if( isset($instance['title']) && strlen($instance['title'])>1 ) 
		{
			$title = $instance[ 'title' ];
		}

		if( isset($instance['sorting_algorithm']) && intval($instance['sorting_algorithm'])>0 ) 
		{
			$sorting_algorithm = intval($instance['sorting_algorithm']);
		}
		
		if( isset($instance['count']) && intval($instance['count'])>0 ) 
		{
			$count = intval($instance['count']);
		}
		
		if( isset($instance['cache_lifetime']) && intval($instance['cache_lifetime'])>0 ) 
		{
			$cache_lifetime = intval($instance['cache_lifetime']);
		}
		
		if( isset($instance['one_element_html']) && strlen($instance['one_element_html'])>1 ) 
		{
			$one_element_html = $instance['one_element_html'];
		}
		
		if( isset($instance['post_categories_separator']) && strlen($instance['post_categories_separator'])>0 ) 
		{
			$post_categories_separator = $instance['post_categories_separator'];
		}
		
		if( isset($instance['post_date_format']) && strlen($instance['post_date_format'])>0 ) 
		{
			$post_date_format = $instance['post_date_format'];
		}
				
		echo(
		    '<p>'.
		        '<label for="'.$this->get_field_id('title').'">Widget title:</label>'.
		        '<input class="widefat" id="'.$this->get_field_id('title').'" name="'.$this->get_field_name('title').'" type="text" value="'.esc_attr($title).'" />'.
		    '</p>'.
		    '<p>'.
		        '<label for="'.$this->get_field_id('sorting_algorithm').'">Sorting algorithm:</label>'.
		        '<select class="widefat" id="'.$this->get_field_id('sorting_algorithm').'" name="'.$this->get_field_name('sorting_algorithm').'">'.
		            '<option value="1"'.( $sorting_algorithm<2 || $sorting_algorithm>3 ? ' selected="selected"' : '' ).'>Hits count</option>'.
		            '<option value="2"'.( $sorting_algorithm==2 ? ' selected="selected"' : '' ).'>Comments count</option>'.
		            '<option value="3"'.( $sorting_algorithm==3 ? ' selected="selected"' : '' ).'>Hits count + 10 * Comments count</option>'.
		        '</select>'.
		    '</p>'.		    
		    '<p>'.
		        '<label for="'.$this->get_field_id('count').'">Display count:</label>'.
		        '<input class="widefat" id="'.$this->get_field_id('count').'" name="'.$this->get_field_name('count').'" type="text" value="'.esc_attr($count).'" />'.
		    '</p>'.
		    '<p>'.
		        '<label for="'.$this->get_field_id('cache_lifetime').'">Cache lifetime (sec):</label>'.
		        '<input class="widefat" id="'.$this->get_field_id('cache_lifetime').'" name="'.$this->get_field_name('cache_lifetime').'" type="text" value="'.esc_attr($cache_lifetime).'" />'.
		    '</p>'.
		    '<p>'.
		        '<label for="'.$this->get_field_id('one_element_html').'">One element HTML (inside &lt;LI&gt;):</label>'.
		        '<textarea class="widefat" cols="20" rows="5" id="'.$this->get_field_id('one_element_html').'" name="'.$this->get_field_name('one_element_html').'">'.$one_element_html.'</textarea>'.
                'You can use this placeholders:'.
                '<ul>'.
		            '<li><code>{post_id}</code> - Post ID</li>'.
		            '<li><code>{post_title}</code> - Post title</li>'.
		            '<li><code>{post_excerpt_N}</code> - Post excerpt, where <code>N</code> - is words count<br />&nbsp;&nbsp;For example: <code>{post_excerpt_10}</code> or <code>{post_excerpt_255}</code></li>'.
		            '<li><code>{post_author}</code> - Post author name</li>'.
		            '<li><code>{post_author_link}</code> - Post author link</li>'.
		            '<li><code>{permalink}</code> - Post link</li>'.
		            '<li><code>{post_date}</code> - Post date</li>'.
		            '<li><code>{thumbnail-[medium|...|64x64]}</code> - Post thumbnail with size<br />&nbsp;&nbsp;For example: <code>{thumbnail-large}</code> or <code>{thumbnail-320x240}</code>'.
		            '<li><code>{post_categories}</code> - Links to post categories with <code>'.$post_categories_separator.'</code> as separator</li>'.
		            '<li><code>{post_hits}</code> - Post hits, counted by this plugin</li>'.
		            '<li><code>{post_comments_count}</code> - Post comments count</li>'.
	            '</ul>'.
		    '</p>'.
		    '<p>'.
		        '<label for="'.$this->get_field_id('post_categories_separator').'">Categories separator:</label>'.
		        '<input class="widefat" id="'.$this->get_field_id('post_categories_separator').'" name="'.$this->get_field_name('post_categories_separator').'" type="text" value="'.esc_attr($post_categories_separator).'" />'.
		    '</p>'.
		    '<p>'.
		        '<label for="'.$this->get_field_id('post_date_format').'">Date format (for more info see <a href="http://php.net/manual/en/function.date.php" target="_BLANK">date() manual</a>):</label>'.
		        '<input class="widefat" id="'.$this->get_field_id('post_date_format').'" name="'.$this->get_field_name('post_date_format').'" type="text" value="'.esc_attr($post_date_format).'" />'.
		    '</p>'.
		    '<p>'.
		        'I forgot something? <a href="http://wordpress.org/support/plugin/ajax-hits-counter" target="_BLANK">You can write to me!</a>'.
		    '</p>'
		    );
	}

    ///////////////////////////////////////////////////////////////////////////
}

///////////////////////////////////////////////////////////////////////////////

// register AJAX Hits Counter: Popular Posts Widget
add_action( 'widgets_init', create_function( '', 'register_widget( "AJAX_Hits_Counter_Popular_Posts_Widget" );' ) );

// remove cached data
add_action( 'save_post', array( 'AJAX_Hits_Counter_Popular_Posts_Widget', 'clearCache' ) );

///////////////////////////////////////////////////////////////////////////////

